<!--
/*******************************************************************************
 * Copyright (c) 2019 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
-->
<!DOCTYPE html>
<meta charset="utf-8">

<link rel="stylesheet" href="graphmetrics/bootstrap-4.3.1-dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="graphmetrics/css/themes.css" title="Themes">

<head>
  <title>Codewind Application Metrics</title>
</head>

<body>
  <!-- load the d3.js library -->
  <script src="graphmetrics/d3/d3.v3.min.js"></script>
  <script src="graphmetrics/jquery/jquery-3.5.1.min.js"></script>
  <script src="graphmetrics/bootstrap-4.3.1-dist/js/bootstrap.min.js"></script>
  <script>
    const queryStringValues = new URLSearchParams(window.location.search)
    if (queryStringValues.get('theme') === 'dark') {
      $('body').addClass("darkmode")
    } else {
      $('body').addClass("lightmode")
    }
  </script>

  <div class="headerDiv">
    <span class="leftHeader">Codewind Application Metrics</span>
    <span class="rightHeader">
      <a class="docLink" href="https://www.eclipse.org/codewind/" target="_blank">
        Go To Documentation
      </a>
    </span>
  </div>

  <div id="tabs" class="container">

    <ul class="nav nav-tabs">
      <li class="active">
        <a href="#dashboard" id="dashboard-tab" data-toggle="tab">Dashboard</a>
      </li>
      <li>
        <a href="#summary" id="summary-tab" data-toggle="tab">Summary</a>
      </li>
    </ul>

    <div class="tab-content ">
      <div class="tab-pane active" id="dashboard">
        <div class="container-fluid">
          <div class="row">
            <div class="col-md-6 hideable" id="cpuDiv1"></div>
            <div class="col-md-6 hideable" id="httpDiv1"></div>
          </div>
          <div class="row">
            <div class="col-md-6 hideable" id="memPoolsDiv"></div>
            <div class="col-md-6 hideable" id="httpDiv2"></div>
          </div>
          <div class="row">
            <div class="col-md-6 hideable" id="gcDiv1"></div>
            <div class="col-md-6 hideable" id="httpDiv3"></div>
          </div>
        </div>
      </div>

      <div class="tab-pane" id="summary">
        <div class="container-fluid">
          <div class="row">
            <div class="graph-container col-md-7">
              <div class="col-md-12 height-fill hideable" id="httpSummaryDiv"></div>
            </div>
            <div class="graph-container col-md-5">
              <div class="col-md-12 hideable" id="envDiv"></div>
              <div class="col-md-12 hideable" id="summaryDiv"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    // Global variables
    var monitoringStartTime = new Date();
    var maxTimeWindow = 900000; // 15 minutes
    let localizedStrings = {};

    // Initialise graph and canvas dimensions
    var margin = {
      top: 50,
      right: 20,
      bottom: 50,
      shortBottom: 30,
      left: 60
    },
      canvasWidth = $("#cpuDiv1").width() - 8; // -8 for margins and borders
    httpCanvasWidth = $("#httpDiv1").width() - 8;
    memPoolsCanvasWidth = $("#memPoolsDiv").width() - 8;
    graphWidth = canvasWidth - margin.left - margin.right;
    httpGraphWidth = httpCanvasWidth - margin.left - margin.right;
    memPoolsGraphWidth = memPoolsCanvasWidth - margin.left - margin.right;
    canvasHeight = 250;
    tallerGraphHeight = canvasHeight - margin.top - margin.shortBottom;
    graphHeight = canvasHeight - margin.top - margin.bottom;

    let myurl = location.host;

    // set up http variables to hold data for top 5 and averages.
    var httpRate = [];
    var httpAverages = new Object;

    function getTimeFormat() {
      var currentTime = new Date()
      if (currentTime.getMinutes() - monitoringStartTime.getMinutes() >= 3
        || currentTime.getHours() > monitoringStartTime.getHours()) {
        return d3.time.format("%H:%M");
      } else {
        return d3.time.format("%H:%M:%S");
      }
    }
  </script>

  <script type="text/javascript" src="graphmetrics/js/i18n.js"></script>
  <script>
    populateLocalizedStrings();
  </script>

  <script type="text/javascript" src="graphmetrics/js/header.js"></script>
  <script type="text/javascript" src="graphmetrics/js/textTable.js"></script>
  <script type="text/javascript" src="graphmetrics/js/cpuChart.js"></script>
  <script type="text/javascript" src="graphmetrics/js/httpThroughPutChart.js"></script>
  <script type="text/javascript" src="graphmetrics/js/httpRequestsChart.js"></script>
  <script type="text/javascript" src="graphmetrics/js/httpSummary.js"></script>
  <script type="text/javascript" src="graphmetrics/js/top5.js"></script>
  <script type="text/javascript" src="graphmetrics/js/gcTimeChart.js"></script>
  <script type="text/javascript" src="graphmetrics/js/memPoolsChart.js"></script>
  <script type="module" src="parse-prometheus-text-format.js"></script>
  <script src="scripts/pfeRequester.js"></script>
  <script>
    let hostname = location.host;
    let pathname = location.pathname;
    let dashboardRoot = "/" + pathname.split('/')[1];

    // CPU Summary data
    let totalProcessCPULoad = 0.0;
    let totalSystemCPULoad = 0.0;
    let numCPULoadSamples = 0;

    let summary = {
      cpu: {},
      gc: {},
      memoryPools: {},
    };

    let envTable = new TextTable('#envDiv', '#summary', localizedStrings.envTitle);
    let summaryTable = new TextTable('#summaryDiv', '#summary', localizedStrings.summaryTitle);
    let httpSummary = new HttpSummary('#httpSummaryDiv', '#summary', localizedStrings.httpSummaryTitle);
    httpSummary.setHttpSummaryOptions({ host: hostname, filteredPath: dashboardRoot });

    pollMetricsAndUpdateDash(maxPolls, updateDash);

    function updateDash(projectData) {
      // console.log('projectData');
      // console.log(projectData);
      if (projectData.metrics) {
        const combinedMetrics = combineMetrics(projectData.metrics);
        updateGraphs(combinedMetrics);
      }
    }

    function combineMetrics(metrics) {
      // console.log('metrics');
      // console.log(metrics);
      let metricsFromUser = [];
      let metricsFromCodewind = [];

      if (metrics.fromUser) {
        metricsFromUser = parsePrometheusTextFormat(metrics.fromUser);
        // console.log('metricsFromUser');
        // console.log(metricsFromUser);
      }

      if (metrics.fromCodewind) {
        metricsFromCodewind = parsePrometheusTextFormat(metrics.fromCodewind);
        // console.log('metricsFromCodewind');
        // console.log(metricsFromCodewind);
      }

      const metricsFromUserNotProvidedByCodewind = metricsFromUser.filter(userMetric =>
        !metricsFromCodewind.some(cwMetric => cwMetric.name === userMetric.name)
      );
      // console.log('metricsFromUserNotProvidedByCodewind');
      // console.log(metricsFromUserNotProvidedByCodewind);

      const combinedMetrics = metricsFromCodewind.concat(metricsFromUserNotProvidedByCodewind);
      // console.log('combinedMetrics');
      // console.log(combinedMetrics);
      return combinedMetrics;
    }

    function updateGraphs(metrics) {
      const time = Date.now();
      const cpuData = { time };
      const gcData = { time };
      const memPoolsData = { time };
      const httpRequestData = { time };
      const envData = [];
      const alltimeDataAboutHttpRequests = {};
      for (const metric of metrics) {
        const metricsObj = metric.metrics[0];
        const metricValue = metricsObj.value;
        let numMetricValue = metricValue * 1;
        if (Number.isNaN(numMetricValue)) {
          numMetricValue = 0;
        }
        if (['base:cpu_process_cpu_load_percent', 'base_cpu_processCpuLoad_percent', 'process_cpu_used_ratio '].includes(metric.name)) {
          cpuData.process = numMetricValue;
        } else if (['os_cpu_used_ratio'].includes(metric.name)) { // ignored if -1 (not available)
          cpuData.system = numMetricValue;
        } else if (['base:gc_global_time_seconds', 'overall_time_in_gc_percentage'].includes(metric.name)) {
          gcData.gcTime = numMetricValue;
        } else if (['base_gc_time_seconds'].includes(metric.name)) {
          const correctMetricsObj = metric.metrics.find(obj => obj.labels.name === 'global');
          gcData.gcTime = correctMetricsObj.value;
          if (Number.isNaN(gcData.gcTime)) {
            gcData.gcTime = 0;
          }
        } else if (['environment_variable'].includes(metric.name)) {
          for (const entry of metric.metrics) {
            const envParam = Object.getOwnPropertyNames(entry.labels)[0];
            const envEntry = { Parameter: envParam, Value: entry.labels[envParam] };
            envData.push(envEntry);
          }
        } else if (metric.name.includes('http_request_')) {
          for (const metricsObj of metric.metrics) {
            const { handler, method } = metricsObj.labels;
            const endpoint = `${method.toUpperCase()} ${handler}`;
            if (!alltimeDataAboutHttpRequests.hasOwnProperty(endpoint)) {
              alltimeDataAboutHttpRequests[endpoint] = {
                url: endpoint,
              };
            }
            const alltimeData = alltimeDataAboutHttpRequests[endpoint];
            if (metric.name === 'http_request_total') {
              alltimeData.hits = metricsObj.value;
            } else if (metric.name === 'http_request_duration_avg_microseconds') {
              alltimeData.averageResponseTime = metricsObj.value;
            } else if (metric.name === 'http_request_duration_max_microseconds') {
              alltimeData.longestResponseTime = metricsObj.value;
        }
      }
        } else if (['base_memory_usedHeap_bytes', 'process_heap_memory_used_bytes', 'base:memory_used_heap_bytes'].includes(metric.name)) {
          memPoolsData.usedHeap = numMetricValue;
        } else if (metric.name === 'process_native_memory_used_bytes') {
          memPoolsData.usedNative = numMetricValue;
        } else if (metric.name === 'process_heap_memory_used_after_GC_bytes') {
          memPoolsData.usedHeapAfterGC = numMetricValue;
        } else if (metric.name === 'http_requests_duration_max_microseconds') {
          httpRequestData.longest = numMetricValue;
          // only ever one url
          httpRequestData.url = metric.metrics[0].labels.handler;
        } else if (metric.name === 'http_requests_duration_average_microseconds') {
          httpRequestData.average = numMetricValue;
        } else if (metric.name === 'http_requests_total') {
          httpRequestData.total = numMetricValue;
        }
      }

      if (Object.keys(cpuData).length > 1) {
        processCPUData(
          cpuData,
          totalProcessCPULoad,
          totalSystemCPULoad,
          numCPULoadSamples,
        );
      }
      if (Object.keys(gcData).length > 1) {
        processGCData(gcData);
      }
      if (Object.keys(memPoolsData).length > 1) {
        processMemPoolsData(memPoolsData);
      }
      if (Object.keys(httpRequestData).length > 1) {
        console.log('+++ DEBUG +++')
        console.dir(httpRequestData)
        processHttpRequestData(httpRequestData);
      }
      httpSummary.updateURLData(JSON.stringify(Object.values(alltimeDataAboutHttpRequests)));
      updateSummaryTable();
      envTable.populateTable(envData);
    }

    // Original code to update graphs:
    // They expect the following objects:
      //  {
      //   time: 1572617286795,
      //   system: 0.022906090659757385,
      //   process: 0.0037501107295195104, // base_cpu_processCpuLoad_percent
      //   processMean: 0.003553777086287611,
      //   systemMean: 0.01992974160373655
      // },
      // {
      //   time: 1572617286795,
      //   gcTime: 0.2022953932836541658
      // },
      // {
      //   time: 1572617288774,
      //   usedHeapAfterGC: 31790744,
      //   usedHeap: 33916696, // base_memory_usedHeap_bytes
      //   usedNative: 78404548,
      //   usedHeapAfterGCMax: 41781688,
      //   usedNativeMax: 207693020
      // }
    // Code:
    // client.onmessage = function (message) {
    //   received = JSON.parse(message.data);
    //   topic = received.topic;
    //   payload = JSON.stringify(received.payload);

    //   switch (topic) {
    //     case 'cpu':
    //       updateCPUData(payload);
    //       summary.cpu.systemMean = received.payload.systemMean;
    //       summary.cpu.processMean = received.payload.processMean;
    //       break;
    //     case 'gc':
    //       updateGCData(payload);
    //       summary.gc.time = received.payload.gcTime;
    //       break;
    //     case 'env':
    //       envTable.populateTableJSON(payload);
    //       break;
    //     case 'http':
    //       updateHttpData(payload);
    //       break;
    //     case 'httpURLs':
    //       httpSummary.updateURLData(payload);
    //       break;
    //     case 'title':
    //       updateHeader(payload);
    //       break;
    //     case 'memoryPools':
    //       updateMemPoolsData(payload);
    //       summary.memoryPools = {
    //         usedHeapAfterGCAverage: received.payload.usedHeapAfterGCAverage,
    //         usedHeapAfterGCMax: received.payload.usedHeapAfterGCMax,
    //         usedNativeMax: received.payload.usedNativeMax
    //       };
    //       break;
    //   }
    const normalisationFactor = 0.6;

    function deepClone(obj) {
      return JSON.parse(JSON.stringify(obj));
    }

    function processCPUData(originalData, totalProcessCPULoad, totalSystemCPULoad, numCPULoadSamples) {
      const data = deepClone(originalData);
      ['system', 'process'].forEach(category => {
        const dataValue = parseFloat(data[category], 10);
        data[category] = (dataValue < 0) ? 0 : dataValue * normalisationFactor;
      });
      updateCPUData(JSON.stringify(data));

      totalProcessCPULoad += data.process;
      totalSystemCPULoad += data.system;
      numCPULoadSamples++;
      const processMean = (totalProcessCPULoad / numCPULoadSamples);
      const systemMean = (totalSystemCPULoad / numCPULoadSamples);

      summary.cpu.processMean = processMean;
      summary.cpu.systemMean = systemMean;
    }
    function processGCData(data) {
      updateGCData(JSON.stringify(data));
      summary.gc.time = data.gcTime;
    }
    function processMemPoolsData(data) {
      updateMemPoolsData(JSON.stringify(data));
      // Commented out as `data` doesn't currently get any of these properties
      // summary.memoryPools = {
      //   usedHeapAfterGCAverage: data.usedHeapAfterGCAverage,
      //   usedHeapAfterGCMax: data.usedHeapAfterGCMax,
      //   usedNativeMax: data.usedNativeMax,
      // };
    }
    function processHttpRequestData(data) {
      updateHttpData(JSON.stringify(data));
    }

    function updateSummaryTable() {
      let summaryData = [];
      if (summary.cpu.processMean) {
        let value = new Number(summary.cpu.processMean);
        let valueStr = value.toLocaleString([], { style: 'percent', minimumSignificantDigits: 4, maximumSignificantDigits: 4 });
        summaryData.push({ Parameter: 'Average Process CPU', Value: valueStr });
      }
      if (summary.cpu.systemMean) {
        let value = new Number(summary.cpu.systemMean);
        let valueStr = value.toLocaleString([], { style: 'percent', minimumSignificantDigits: 4, maximumSignificantDigits: 4 });
        summaryData.push({ Parameter: 'Average System CPU', Value: valueStr });
      }
      if (summary.gc.time) {
        let value = new Number(summary.gc.time);
        let valueStr = value.toLocaleString([], { style: 'percent', minimumSignificantDigits: 4, maximumSignificantDigits: 4 });
        summaryData.push({ Parameter: 'Time Spent in GC', Value: `${valueStr}` });
      }
      if (summary.memoryPools.usedHeapAfterGCMax) {
        summaryData.push({ Parameter: 'Max Heap Used After GC', Value: `${summary.memoryPools.usedHeapAfterGCMax} bytes` });
        summaryData.push({ Parameter: 'Max Native Heap Used', Value: `${summary.memoryPools.usedNativeMax} bytes` });
      }
      summaryTable.populateTable(summaryData);
    }
  </script>
  <script>

    let selected_tab = "dashboard-tab"
    window.addEventListener('resize', resize);

    // Also re-size when we change tabs in case we re-sized
    // while the new tab wasn't visible.
    $('.nav-tabs a').on('shown.bs.tab', function (event) {
      selected_tab = event.target.id;
      resize();
    });

    function resize() {
      if (selected_tab == "dashboard-tab") {
        canvasWidth = $("#cpuDiv1").width() - 8,
          httpCanvasWidth = $("#httpDiv1").width() - 8,
          memPoolsCanvasWidth = $("#memPoolsDiv").width() - 8,
          graphWidth = canvasWidth - margin.left - margin.right,
          httpGraphWidth = httpCanvasWidth - margin.left - margin.right,
          memPoolsGraphWidth = memPoolsCanvasWidth - margin.left - margin.right,
          resizeCPUChart();
        resizeHttpChart();
        resizeHttpThroughputChart();
        resizeGCChart();
        resizeMemPoolsChart();
      } else if (selected_tab == "summary-tab") {
        envTable.resizeTable();
        summaryTable.resizeTable();
        httpSummary.resizeTable();
      }
    }

  </script>

</body>

</html>
